﻿//-----------------------------------------------------------------------
// <copyright file="StaticRules.cs" company="None">
//     Copyright (c) 2009-2012
// </copyright>
//-----------------------------------------------------------------------

namespace CleanCode.StyleCopCustomRules
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using StyleCop.CSharp;

    /// <summary>
    /// Represents a container for all static StyleCop rule delegates used by the <see cref="CleanCodeRulesAnalyzer"/>.
    /// </summary>
    internal static class StaticRules
    {
        /// <summary>
        /// Holds the list of boundary conditions.
        /// </summary>
        private static readonly IList<string> BoundaryConditionList = new List<string>();

        /// <summary>
        /// Checks the elements for too many parameters.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true if the following rules shall be checked, false if the check shall be aborted.
        /// </returns>
        internal static bool CheckElementsForTooManyParameters(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (element.ElementType == ElementType.Method)
            {
                var method = (Method)element;
                if (HasDllImportAttribute(method))
                {
                    return true;
                }

                int actualNumberOfParameters = method.Parameters.Count;
                int parameterLimit = context.GetPropertyValue<int>(element.Document as CsDocument, "ParameterLimit");
                if (actualNumberOfParameters > parameterLimit)
                {
                    context.AddViolation(
                        method,
                        "TooManyParameters",
                        method.Declaration.Name,
                        actualNumberOfParameters,
                        parameterLimit);
                }
            }

            return true;
        }

        /// <summary>
        /// Checks all methods for the limited length of lines.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        internal static bool CheckElementsForTooLongMethods(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (element.ElementType == ElementType.Method)
            {
                int numberOfLinesInMethod = GetNumberOfCodeLinesInMethod(element);
                const int AllowedNumberOfLinesInMethod = 30;
                if (numberOfLinesInMethod > AllowedNumberOfLinesInMethod)
                {
                    context.AddViolation(
                        element,
                        "TooLong",
                        element.Declaration.Name,
                        numberOfLinesInMethod,
                        AllowedNumberOfLinesInMethod);
                }
            }

            return true;
        }

        /// <summary>
        /// Checks all methods for the occurence of an output parameter.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        internal static bool CheckElementsForOutputParameters(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (element.ElementType == ElementType.Method)
            {
                var method = (Method)element;
                foreach (var parameter in method.Parameters)
                {
                    if (parameter.Modifiers == ParameterModifiers.Out)
                    {
                        context.AddViolation(
                            method,
                            "OutputParameters",
                            method.Declaration.Name);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Checks all methods for the occurence of a flag (boolean) parameter except the Dispose(bool) method which is commonly
        /// used to support the implementation of IDisposable interface.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        internal static bool CheckElementsForFlagParameters(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (element.ElementType == ElementType.Method)
            {
                var method = (Method)element;
                foreach (var parameter in method.Parameters)
                {
                    if (MethodHasBoolParamButIsNotDisposeMethod(method, parameter))
                    {
                        context.AddViolation(
                            method,
                            "FlagParameters",
                            method.Declaration.Name);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Checks all methods for the occurence of a switch statement.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        internal static bool CheckElementsForSwitchStatement(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (element.ElementType == ElementType.Method)
            {
                var method = (Method)element;

                // Not very efficient, but I didn't found a way to trace back to the current method 
                // when I walk down the tokens instead of the elements.
                foreach (var token in method.Tokens)
                {
                    if (token.CsTokenType == CsTokenType.Switch)
                    {
                        context.AddViolation(
                            element, 
                            element.LineNumber, 
                            "PreferPolymorphismToSwitchCase",
                            method.Declaration.Name);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Checks expressions for the occurence of unencapsulated conditionals.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        [SuppressMessage("CleanCode.StyleCopCustomRules.CleanCodeRulesAnalyzer", "CC4601:TooManyParameters",
            Justification = "It's a StyleCop delegate.")]        
        internal static bool CheckExpressionForUnencapsulatedConditionals(Expression expression, Expression parentExpression, Statement parentStatement, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (IsUnencapsulatedConditional(expression))
            {
                context.AddViolation(
                    parentElement, 
                    expression.LineNumber, 
                    "EncapsulateConditionals",
                    expression.Text);
            }

            return true;
        }

        /// <summary>
        /// Checks the expression for magic numbers.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        [SuppressMessage("CleanCode.StyleCopCustomRules.CleanCodeRulesAnalyzer", "CC4601:TooManyParameters",
            Justification = "It's a StyleCop delegate.")]
        internal static bool CheckExpressionForMagicNumbers(Expression expression, Expression parentExpression, Statement parentStatement, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (ContainsMagicNumber(expression, parentExpression))
            {
                context.AddViolation(
                    parentElement,
                    expression.LineNumber,
                    "ReplaceMagicNumbersWithNamedConstants",
                    expression.Text);
            }

            return true;
        }

        /// <summary>
        /// Checks the expression for negative conditionals.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        [SuppressMessage("CleanCode.StyleCopCustomRules.CleanCodeRulesAnalyzer", "CC4601:TooManyParameters",
            Justification = "It's a StyleCop delegate.")]
        internal static bool CheckExpressionForNegativeConditionals(Expression expression, Expression parentExpression, Statement parentStatement, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (ContainsNegativeConditionals(expression))
            {
                context.AddViolation(
                    parentElement,
                    expression.LineNumber,
                    "AvoidNegativeConditionals",
                    expression.Text);
            }

            return true;
        }

        /// <summary>
        /// Checks the expression for unencapsulated boundary conditions.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The context.</param>
        /// <returns>
        /// Returns true to continue, false to stop visiting the elements in the code document.
        /// </returns>
        [SuppressMessage("CleanCode.StyleCopCustomRules.CleanCodeRulesAnalyzer", "CC4601:TooManyParameters",
            Justification = "It's a StyleCop delegate.")]
        internal static bool CheckExpressionForUnencapsulatedBoundaryConditions(Expression expression, Expression parentExpression, Statement parentStatement, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (ContainsUnencapsulatedBoundaryConditions(expression))
            {
                context.AddViolation(
                    parentElement,
                    expression.LineNumber,
                    "EncapsulateBoundaryConditions",
                    expression.Text);
            }

            return true;
        }

        /// <summary>
        /// Checks whether given method has a boolean parameter, but is not the commonly used Dispose method.
        /// </summary>
        /// <param name="method">
        /// The method to be checked.
        /// </param>
        /// <param name="parameter">
        /// The parameter type name.
        /// </param>
        /// <returns>
        /// Returns true if method has a boolean parameter, but is not the Dispose method.
        /// </returns>
        private static bool MethodHasBoolParamButIsNotDisposeMethod(Method method, Parameter parameter)
        {
            var parameterTypeName = parameter.Type.ToString();
            const bool DoIgnoreCase = true;
            return parameterTypeName.StartsWith("bool", DoIgnoreCase, CultureInfo.InvariantCulture)
                   && (method.Declaration.Name != "Dispose");
        }

        /// <summary>
        /// Determines whether the specified expression contains unencapsulated boundary conditions.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified expression contains unencapsulated boundary conditions; 
        /// otherwise, <c>false</c>.
        /// </returns>
        private static bool ContainsUnencapsulatedBoundaryConditions(Expression expression)
        {
            // TODO: I don't know how right now -> ExpressionType.Arithmetic (level + 1) -> ExpressionType.Literal (level, 1)
            if (expression.ExpressionType == ExpressionType.Arithmetic)
            {
                if (BoundaryConditionList.Contains(expression.Text))
                {
                    return true;
                }

                BoundaryConditionList.Add(expression.Text);
                //(expression.ChildExpressions[0] as Expression).ExpressionType
                return false;
            }
            //if ((expression.ExpressionType == ExpressionType.Literal)
            //    && (expression.ParentExpression.ExpressionType == ExpressionType.Arithmetic)
            //    && (expression.ParentExpression as ArithmeticExpression).RightHandSide.Text == "1")
            //{
            //    return false;
            //}
            return false;
        }

        /// <summary>
        /// Determines whether the specified expression contains negative conditionals.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the expression contains negative conditionals; otherwise, <c>false</c>.
        /// </returns>
        private static bool ContainsNegativeConditionals(Expression expression)
        {
            return (expression is UnaryExpression)
                && ((expression as UnaryExpression).OperatorType == UnaryExpression.Operator.Not);
        }

        /// <summary>
        /// Determines whether the specified expression contains a magic number 
        /// that violates clean code rules.
        /// </summary>
        /// <param name="expression">The expression.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified expression contains a magic number; otherwise, <c>false</c>.
        /// </returns>
        private static bool ContainsMagicNumber(Expression expression, Expression parentExpression)
        {
            return CanContainMagicNumber(expression)
                   && (IsAcceptableMagicNumber(parentExpression) == false)
                   && IsMagicNumber(expression);
        }

        /// <summary>
        /// Determines whether the specified method has the DLL import attribute.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified method has the DLL import attribute; otherwise, <c>false</c>.
        /// </returns>
        private static bool HasDllImportAttribute(Method method)
        {
            foreach (var attribute in method.Attributes)
            {
                if (attribute.Text.Contains("DllImport"))
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Determines whether the specified expression is an unencapsulated conditional one.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the given expression is an unencapsulated conditional -
        /// for example <code>if (ShouldBeDeleted(timer))</code> is Ok, but <code>if (timer.HasExpired &amp;&amp; !timer.IsRecurrent)</code>
        /// is unencapsulated and violates the clean code rule.
        /// If the check is Ok (encapsulated) <code>IsUnencapsulatedConditional</code> returns <c>false</c>.
        /// </returns>
        private static bool IsUnencapsulatedConditional(Expression expression)
        {
            bool isConditionalLogicExpression = expression is ConditionalLogicalExpression;
            bool result = false;
            if (isConditionalLogicExpression)
            {
                result = expression.Parent is IfStatement;
            }

            return result;
        }

        /// <summary>
        /// Determines whether the specified expression can contain a magic number.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified expression can contain a magic number; otherwise, <c>false</c>.
        /// </returns>
        private static bool CanContainMagicNumber(Expression expression)
        {
            return expression is LiteralExpression;
        }

        /// <summary>
        /// Determines whether the specified parent expression is an accepteable magic number
        /// like within variable and constand declarations, array indexers and unary expressions
        /// for example '-1'.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified expression is an acceptable magic number; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsAcceptableMagicNumber(Expression expression)
        {
            bool isAcceptableMagicNumber = (expression is VariableDeclaratorExpression)
               || (expression is UnaryExpression)
               || (expression is ArrayAccessExpression);
            return isAcceptableMagicNumber;
        }

        /// <summary>
        /// Determines whether the specified expression is a magic number.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns <c>true</c> if the specified expression is a magic number; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsMagicNumber(Expression expression)
        {
            bool isMagicNumber = false;
            if (expression is LiteralExpression)
            {
                var literalExpression = expression as LiteralExpression;
                isMagicNumber = literalExpression.Token.CsTokenType == CsTokenType.Number
                           && literalExpression.Token.CsTokenClass == CsTokenClass.Number;
            }

            return isMagicNumber;
        }

        /// <summary>
        /// Checks whether the given <see cref="Expression"/> is a constant declaration.
        /// </summary>
        /// <param name="expression">The expression to check.</param>
        /// <returns>
        /// Returns true if <see cref="Expression"/> is a constant declaration, false otherwise.
        /// </returns>
        private static bool ExpressionIsConstDeclaration(Expression expression)
        {
            if (expression.FindParentElement() == null)
            {
                return false;
            }

            var constDeclaration = expression.FindParentElement().Parent as VariableDeclarationStatement;
            if (constDeclaration == null)
            {
                return false;
            }

            return constDeclaration.Constant;
        }

        /// <summary>
        /// Gets the number of code lines in a specific method starting with the given <see cref="CsElement"/>.
        /// </summary>
        /// <param name="method">The method to count the code lines for.</param>
        /// <returns>Returns the number of code lines the given method uses.</returns>
        private static int GetNumberOfCodeLinesInMethod(CsElement method)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }

            int firstLineNumber = method.Tokens.First.Value.LineNumber;
            int lastLineNumber = method.Tokens.Last.Value.LineNumber;
            int numberOfLinesInMethod = lastLineNumber - firstLineNumber;
            return numberOfLinesInMethod;
        }
    }
}
